﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace System.Windows.Media.Media3D10
{
    /// <summary>
    /// Matériau de rendu d'une atmosphère
    /// </summary>
    [System.Windows.Markup.ContentProperty("Brush")]
    public sealed class AtmosphereMaterial : Material
    {
        #region Dependency Properties
        /// <summary>
        /// Identifie la propriété de dépendance <see cref="InnerRadiusProperty"/>.
        /// </summary>
        public static readonly DependencyProperty InnerRadiusProperty =
            DependencyProperty.Register("InnerRadius", typeof(float), typeof(AtmosphereMaterial),
            new PropertyMetadata(new PropertyChangedCallback(InnerRadius_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="OuterRadiusProperty"/>.
        /// </summary>
        public static readonly DependencyProperty OuterRadiusProperty =
            DependencyProperty.Register("OuterRadius", typeof(float), typeof(AtmosphereMaterial),
            new PropertyMetadata(new PropertyChangedCallback(OuterRadius_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="OpacityProperty"/>.
        /// </summary>
        public static readonly DependencyProperty OpacityProperty =
            DependencyProperty.Register("Opacity", typeof(float), typeof(AtmosphereMaterial),
            new PropertyMetadata(1.0f, new PropertyChangedCallback(Opacity_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="ScaleDepthProperty"/>.
        /// </summary>
        public static readonly DependencyProperty ScaleDepthProperty =
            DependencyProperty.Register("ScaleDepth", typeof(float), typeof(AtmosphereMaterial),
            new PropertyMetadata(0.25f, new PropertyChangedCallback(ScaleDepthProperty_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="SampleCountProperty"/>.
        /// </summary>
        public static readonly DependencyProperty SampleCountProperty =
            DependencyProperty.Register("SampleCount", typeof(int), typeof(AtmosphereMaterial),
            new PropertyMetadata(2, new PropertyChangedCallback(SampleCount_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="BrushProperty"/>.
        /// </summary>
        public static readonly DependencyProperty BrushProperty =
            DependencyProperty.Register("Brush", typeof(SolidColorBrush), typeof(AtmosphereMaterial),
            new PropertyMetadata(new PropertyChangedCallback(Brush_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="KrProperty"/>.
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709")]
        public static readonly DependencyProperty KrProperty =
            DependencyProperty.Register("Kr", typeof(float), typeof(AtmosphereMaterial),
            new PropertyMetadata(0.0025f, new PropertyChangedCallback(Kr_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="KmProperty"/>.
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709")]
        public static readonly DependencyProperty KmProperty =
            DependencyProperty.Register("Km", typeof(float), typeof(AtmosphereMaterial),
            new PropertyMetadata(0.0015f, new PropertyChangedCallback(Km_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="ESunProperty"/>.
        /// </summary>
        public static readonly DependencyProperty ESunProperty =
            DependencyProperty.Register("ESun", typeof(float), typeof(AtmosphereMaterial),
            new PropertyMetadata(15.0f, new PropertyChangedCallback(ESun_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="GProperty"/>.
        /// </summary>
        public static readonly DependencyProperty GProperty =
            DependencyProperty.Register("G", typeof(float), typeof(AtmosphereMaterial),
            new PropertyMetadata(-0.95f, new PropertyChangedCallback(G_PropertyChanged)));
        #endregion

        #region Dependency Properties Callbacks
        private static void InnerRadius_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            (sender as AtmosphereMaterial).OnInnerRadiusChanged(e);
        }

        private static void OuterRadius_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            (sender as AtmosphereMaterial).OnOuterRadiusChanged(e);
        }

        private static void ScaleDepthProperty_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            (sender as AtmosphereMaterial).OnScaleDepthChanged(e);
        }

        private static void SampleCount_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            (sender as AtmosphereMaterial).OnSampleCountChanged(e);
        }

        private static void Opacity_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            (sender as AtmosphereMaterial).OnOpacityChanged(e);
        }

        private static void Brush_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            (sender as AtmosphereMaterial).OnBrushChanged(e);
        }

        private static void Kr_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            (sender as AtmosphereMaterial).OnKrChanged(e);
        }

        private static void Km_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            (sender as AtmosphereMaterial).OnKmChanged(e);
        }

        private static void ESun_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            (sender as AtmosphereMaterial).OnESunChanged(e);
        }

        private static void G_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            (sender as AtmosphereMaterial).OnGChanged(e);
        }
        #endregion

        #region Constructors
        /// <summary>
        /// Initialise une nouvelle instance de la classe <see cref="AtmosphereMaterial"/>
        /// </summary>
        public AtmosphereMaterial()
            : base()
        {
            this.Brush = new SolidColorBrush(Colors.Blue);
        }
        #endregion

        #region Data
        private const string g_fAtmosphereHeight = "g_fAtmosphereHeight";
        private const string g_fAtmosphereOpacity = "g_fAtmosphereOpacity";
        private const string g_vInvWavelength = "g_vInvWavelength";
        private const string g_fInnerRadius = "g_fInnerRadius";
        private const string g_fInnerRadius2 = "g_fInnerRadius2";
        private const string g_fOuterRadius = "g_fOuterRadius";
        private const string g_fOuterRadius2 = "g_fOuterRadius2";
        private const string g_fCameraHeight = "g_fCameraHeight";
        private const string g_fCameraHeight2 = "g_fCameraHeight2";
        private const string g_fScale = "g_fScale";
        private const string g_fScaleDepth = "g_fScaleDepth";
        private const string g_fScaleOverScaleDepth = "g_fScaleOverScaleDepth";
        private const string g_fKrESun = "g_fKrESun";
        private const string g_fKmESun = "g_fKmESun";
        private const string g_fKr4PI = "g_fKr4PI";
        private const string g_fKm4PI = "g_fKm4PI";
        private const string g_nSamples = "g_nSamples";
        private const string g_fSamples = "g_fSamples";
        private const string g_g = "g_g";
        private const string g_g2 = "g_g2";
        #endregion

        #region Public Properties
        /// <summary>
        /// Rayon intérieur de l'atmosphère
        /// </summary>
        public float InnerRadius
        {
            get { return (float)GetValue(InnerRadiusProperty); }
            set { SetValue(InnerRadiusProperty, value); }
        }

        /// <summary>
        /// Rayon extérieur de l'atmosphère
        /// </summary>
        public float OuterRadius
        {
            get { return (float)GetValue(OuterRadiusProperty); }
            set { SetValue(OuterRadiusProperty, value); }
        }

        /// <summary>
        /// Opacité de l'atmosphère
        /// </summary>
        public float Opacity
        {
            get { return (float)GetValue(OpacityProperty); }
            set { SetValue(OpacityProperty, value); }
        }

        /// <summary>
        /// Hauteur moyenne de l'amosphère
        /// </summary>
        public float ScaleDepth
        {
            get { return (float)GetValue(ScaleDepthProperty); }
            set { SetValue(ScaleDepthProperty, value); }
        }

        /// <summary>
        /// Nombre de samples à calculer
        /// </summary>
        public int SampleCount
        {
            get { return (int)GetValue(SampleCountProperty); }
            set { SetValue(SampleCountProperty, value); }
        }

        /// <summary>
        /// Couleur de l'atmosphère
        /// </summary>
        public SolidColorBrush Brush
        {
            get { return (SolidColorBrush)GetValue(BrushProperty); }
            set { SetValue(BrushProperty, value); }
        }

        /// <summary>
        /// Coefficient de Rayleigh
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709")]
        public float Kr
        {
            get { return (float)GetValue(KrProperty); }
            set { SetValue(KrProperty, value); }
        }

        /// <summary>
        /// Coefficient de Mie
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709")]
        public float Km
        {
            get { return (float)GetValue(KmProperty); }
            set { SetValue(KmProperty, value); }
        }

        /// <summary>
        /// Exposition du soleil
        /// </summary>
        public float ESun
        {
            get { return (float)GetValue(ESunProperty); }
            set { SetValue(ESunProperty, value); }
        }

        /// <summary>
        /// Coefficient G
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704")]
        public float G
        {
            get { return (float)GetValue(GProperty); }
            set { SetValue(GProperty, value); }
        }
        #endregion

        #region Private Methods
        private void OnInnerRadiusChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");

            if (!object.ReferenceEquals(this.Effect, null))
            {
                float height = OuterRadius - InnerRadius;
                float innerRadius2 = InnerRadius * InnerRadius;
                float scale = 1.0f / height;
                float scaleOverScaleDepth = scale / ScaleDepth;

                this.Effect.GetVariableByName(g_fAtmosphereHeight).AsScalar().Set(height);
                this.Effect.GetVariableByName(g_fInnerRadius).AsScalar().Set(InnerRadius);
                this.Effect.GetVariableByName(g_fInnerRadius2).AsScalar().Set(innerRadius2);
                this.Effect.GetVariableByName(g_fScale).AsScalar().Set(scale);
                this.Effect.GetVariableByName(g_fScaleOverScaleDepth).AsScalar().Set(scaleOverScaleDepth);
            }
        }

        private void OnOuterRadiusChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");

            if (!object.ReferenceEquals(this.Effect, null))
            {
                float height = OuterRadius - InnerRadius;
                float outerRadius2 = OuterRadius * OuterRadius;
                float scale = 1.0f / height;
                float scaleOverScaleDepth = scale / ScaleDepth;

                this.Effect.GetVariableByName(g_fOuterRadius).AsScalar().Set(OuterRadius);
                this.Effect.GetVariableByName(g_fOuterRadius2).AsScalar().Set(outerRadius2);
                this.Effect.GetVariableByName(g_fScale).AsScalar().Set(scale);
                this.Effect.GetVariableByName(g_fScaleOverScaleDepth).AsScalar().Set(scaleOverScaleDepth);
            }
        }

        private void OnOpacityChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            if (!object.ReferenceEquals(this.Effect, null))
                this.Effect.GetVariableByName(g_fAtmosphereOpacity).AsScalar().Set(Opacity);
        }

        private void OnScaleDepthChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            if (!object.ReferenceEquals(this.Effect, null))
            {
                float scale = 1.0f / (OuterRadius - InnerRadius);
                float scaleOverScaleDepth = scale / ScaleDepth;
                this.Effect.GetVariableByName(g_fScale).AsScalar().Set(scale);
                this.Effect.GetVariableByName(g_fScaleDepth).AsScalar().Set(ScaleDepth);
                this.Effect.GetVariableByName(g_fScaleOverScaleDepth).AsScalar().Set(scaleOverScaleDepth);
            }
        }

        private void OnSampleCountChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
            if (!object.ReferenceEquals(this.Effect, null))
            {
                this.Effect.GetVariableByName(g_nSamples).AsScalar().Set(SampleCount);
                this.Effect.GetVariableByName(g_fSamples).AsScalar().Set(SampleCount);
            }
        }

        private void OnBrushChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");

            if (!object.ReferenceEquals(this.Effect, null))
            {
                SlimDX.Vector3 waveLength = new SlimDX.Vector3(675 - Brush.Color.R, 675 - Brush.Color.G, 675 - Brush.Color.B) / 1000;
                SlimDX.Vector3 invWavelength = new SlimDX.Vector3(
                    1.0f / (float)Math.Pow(waveLength.X, 4.0f),
                    1.0f / (float)Math.Pow(waveLength.Y, 4.0f),
                    1.0f / (float)Math.Pow(waveLength.Z, 4.0f));
                this.Effect.GetVariableByName(g_vInvWavelength).AsVector().Set(invWavelength);
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709")]
        private void OnKrChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");

            if (!object.ReferenceEquals(this.Effect, null))
            {
                float KrESun = Kr * ESun;
                float Kr4PI = Kr * 4 * (float)Math.PI;
                this.Effect.GetVariableByName(g_fKrESun).AsScalar().Set(KrESun);
                this.Effect.GetVariableByName(g_fKr4PI).AsScalar().Set(Kr4PI);
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709")]
        private void OnKmChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");

            if (!object.ReferenceEquals(this.Effect, null))
            {
                float KmESun = Km * ESun;
                float Km4PI = Km * 4 * (float)Math.PI;
                this.Effect.GetVariableByName(g_fKmESun).AsScalar().Set(KmESun);
                this.Effect.GetVariableByName(g_fKm4PI).AsScalar().Set(Km4PI);
            }
        }

        private void OnESunChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");

            if (!object.ReferenceEquals(this.Effect, null))
            {
                float KrESun = Kr * ESun;
                float KmESun = Km * ESun;
                this.Effect.GetVariableByName(g_fKrESun).AsScalar().Set(KrESun);
                this.Effect.GetVariableByName(g_fKmESun).AsScalar().Set(KmESun);
            }
        }

        private void OnGChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");

            if (!object.ReferenceEquals(this.Effect, null))
            {
                float g2 = G * G;
                this.Effect.GetVariableByName(g_g).AsScalar().Set(G);
                this.Effect.GetVariableByName(g_g2).AsScalar().Set(g2);
            }
        }
        #endregion

        #region Material Overrides
        /// <summary>
        /// Appelé lors de l'initialisation de l'effet associé
        /// </summary>
        /// <param name="effect">Effet associé</param>
        protected override void OnInitEffect(SlimDX.Direct3D10.Effect effect)
        {
            base.OnInitEffect(effect);
            if (!object.ReferenceEquals(effect, null))
            {
                if (!object.ReferenceEquals(this.Brush, null))
                {
                    SlimDX.Vector3 waveLength = new SlimDX.Vector3(675 - Brush.Color.R, 675 - Brush.Color.G, 675 - Brush.Color.B) / 1000;
                    SlimDX.Vector3 invWavelength = new SlimDX.Vector3(
                        1.0f / (float)Math.Pow(waveLength.X, 4.0f),
                        1.0f / (float)Math.Pow(waveLength.Y, 4.0f),
                        1.0f / (float)Math.Pow(waveLength.Z, 4.0f));
                    this.Effect.GetVariableByName(g_vInvWavelength).AsVector().Set(invWavelength);
                }

                this.Effect.GetVariableByName(g_fAtmosphereOpacity).AsScalar().Set(Opacity);
                
                float height = OuterRadius - InnerRadius;
                float innerRadius2 = InnerRadius * InnerRadius;
                float outerRadius2 = OuterRadius * OuterRadius;
                float scale = 1.0f / height;
                float scaleOverScaleDepth = scale / ScaleDepth;

                this.Effect.GetVariableByName(g_fOuterRadius).AsScalar().Set(OuterRadius);
                this.Effect.GetVariableByName(g_fOuterRadius2).AsScalar().Set(outerRadius2);
                this.Effect.GetVariableByName(g_fInnerRadius).AsScalar().Set(InnerRadius);
                this.Effect.GetVariableByName(g_fInnerRadius2).AsScalar().Set(innerRadius2);
                this.Effect.GetVariableByName(g_fScale).AsScalar().Set(scale);
                this.Effect.GetVariableByName(g_fScaleDepth).AsScalar().Set(ScaleDepth);
                this.Effect.GetVariableByName(g_fScaleOverScaleDepth).AsScalar().Set(scaleOverScaleDepth);

                float KmESun = Km * ESun;
                float Km4PI = Km * 4 * (float)Math.PI;
                float KrESun = Kr * ESun;
                float Kr4PI = Kr * 4 * (float)Math.PI;
                this.Effect.GetVariableByName(g_fKrESun).AsScalar().Set(KrESun);
                this.Effect.GetVariableByName(g_fKr4PI).AsScalar().Set(Kr4PI);
                this.Effect.GetVariableByName(g_fKmESun).AsScalar().Set(KmESun);
                this.Effect.GetVariableByName(g_fKm4PI).AsScalar().Set(Km4PI);

                this.Effect.GetVariableByName(g_nSamples).AsScalar().Set(SampleCount);
                this.Effect.GetVariableByName(g_fSamples).AsScalar().Set(SampleCount);

                float g2 = G * G;
                this.Effect.GetVariableByName(g_g).AsScalar().Set(G);
                this.Effect.GetVariableByName(g_g2).AsScalar().Set(g2);
            }
        }
        #endregion

        #region Material Implementation
        /// <summary>
        /// En cas de substitution dans une classe dérivée, participe au dessin des objets dans le pipeline DirectX10
        /// </summary>
        /// <param name="context">Contexte de dessin.</param>
        /// <exception cref="ArgumentNullException">Soulevée si le contexte est null.</exception>
        protected override void OnDraw(DrawingContext context)
        {
            if (object.ReferenceEquals(context, null))
                throw new ArgumentNullException("context");

            SlimDX.Vector3 center = SlimDX.Vector3.TransformCoordinate(SlimDX.Vector3.Zero, context.WorldMatrix);
            float cameraHeight = SlimDX.Vector3.Distance(center, context.CameraPosition);
            float cameraHeight2 = cameraHeight * cameraHeight;

            this.Effect.GetVariableByName(g_fCameraHeight).AsScalar().Set(cameraHeight);
            this.Effect.GetVariableByName(g_fCameraHeight2).AsScalar().Set(cameraHeight2);
        }
        #endregion

        #region Freezable Implementation
        /// <summary>
        /// Une fois implémentée dans une classe dérivée, initialise une nouvelle instance de la classe dérivée <see cref="Freezable"/>. 
        /// </summary>
        /// <returns>Nouvelle instance.</returns>
        protected override Freezable CreateInstanceCore()
        {
            return new AtmosphereMaterial();
        }
        #endregion
    }
}
